#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include "serial.h"

// Use a serial port to drive I2C, SPI, and other serial interfaces.

// DB9 RS232 pinout, function, and in or out with respect to a DTE (the computer is a DTE):
// 1 DCD, input
// 2 RX, input
// 3 TX, output
// 4 DTR, output
// 5 gnd
// 6 DSR, input
// 7 RTS, output
// 8 CTS, input
// 9 RI, input (sometimes not implemented)

int AcquireSerialPort(char *port) {
    int fd;
    fd = open(port, O_RDWR);
    return fd;
}

int ReleaseSerialPort(int desc) {
    tcflush(desc, TCIOFLUSH);
    close(desc);
    return 0;
}

int GetCTS(int desc) {
    int status;
    if (ioctl(desc, TIOCMGET, &status) == -1)
        return -1;
    if (status & TIOCM_CTS)
        return 0;
    else
        return 1;
}

int GetDSR(int desc) {
    int status;
    if (ioctl(desc, TIOCMGET, &status) == -1)
        return -1;
    if (status & TIOCM_DSR)
        return 0;
    else
        return 1;
}

int SetDTR(int desc, int state) {
    int status;
    if (ioctl(desc, TIOCMGET, &status) == -1)
        return -1;
    if (state)
        status &= ~TIOCM_DTR;
    else
        status |= TIOCM_DTR;
    ioctl(desc, TIOCMSET, &status); /* set the serial port status */
    return 0;
}

int SetRTS(int desc, int state) {
    int status;
    if (ioctl(desc, TIOCMGET, &status) == -1)
        return -1;
    if (state)
        status &= ~TIOCM_RTS;
    else
        status |= TIOCM_RTS;
    ioctl(desc, TIOCMSET, &status); /* set the serial port status */
    return 0;
}

struct {
    int rate;
    int baud;
} BaudRates[] = {{50, B50}, {75, B75}, {110, B110}, {134, B134}, {150, B150}, {200, B200}, {300, B300}, {600, B600}, {1200, B1200}, {1800, B1800}, {2400, B2400}, {4800, B4800}, {9600, B9600}, {19200, B19200}, {38400, B38400}, {57600, B57600}, {115200, B115200}, {230400, B230400}};

#define MAX_BAUD_RATES (sizeof(BaudRates) / sizeof(BaudRates[0]))
#define SERIAL_CLOCK_PERIOD 10  // clock period in microseconds

// Since DTR and RTS are completely programmable, we use these two as clock and data stream.  On
// the other hand, TXD can be made to act like a pulse of various durations by transmitting a an
// appropriate character at an appropriate bit rate.  Since the start bit is the equivalent of a
// zero, and the stop bit is the equivalent of a 1, we can set the duration of the pulse by
// choosing how many 1s to add in front of the stop bit.  Note that we cannot completely control
// the waveform since the obligatory start bit and stop bit have a fixed relationship.

// Lets assume that the pulse is never there more than 50% of the time.  If we use an 8 bit
// character, we have a total of 10 bits.  Thus, if the first 5 bits are 0s, and the seconds 5
// bits are 1s, we'll have a pulse with a lead-in time the same length as the pulse itself.  The
// bit pattern is 0 00001111 1 which corresponds to a character 11110000 or 0xF0 which is an
// ASCII lower case "p" with the high order bit turned on.  The pulse width is 5 times the bit
// period, which is the inverse of the baud rate.  So, for a pulse of width x in seconds,
// program the baudrate to 5/x Hertz.

int PulseTXD(int desc, int width) {
    int i, baud, flag;
    char pulse_bits;
    struct termios my_termios;

    for (i = 0; i < MAX_BAUD_RATES; i++) {
        baud = 5000000 / width;
        if (baud <= BaudRates[i].rate) {
            baud = BaudRates[i].rate;
            flag = BaudRates[i].baud;
            break;
        }
    }
    if (i == MAX_BAUD_RATES) {
        i = MAX_BAUD_RATES - 1;
        baud = BaudRates[i].rate;
        flag = BaudRates[i].baud;
    }
    if (tcgetattr(desc, &my_termios) < 0) {
        printf("tcgetattr() set errno %d, %s\n", errno, strerror(errno));
        return 1;
    }
    my_termios.c_cflag &= ~CBAUD;
    my_termios.c_cflag |= flag;
    if (tcsetattr(desc, TCSANOW, &my_termios) < 0) {
        printf("tcsetattr() set errno %d, %s\n", errno, strerror(errno));
        return 1;
    }
    pulse_bits = 0xF0;
    if (write(desc, &pulse_bits, 1) != 1) {
        fprintf(stderr, "Couldn't write pulse bits: %s\n", strerror(errno));
        return 1;
    }
    //    tcdrain(desc);
    usleep(1000);
    return 0;
 }

// Negate the clock, set the bit value, wait half a period, set the clock, set the bit value,
// wait half a period.  Use DTR as clock and RTS as data.
int WriteSerialBit(int desc, int bit, int period) {
    // set data
    SetRTS(desc, bit);
    // negate clock
    SetDTR(desc, 1);
    // wait half period
    usleep(period >> 1);
    // set data
    SetRTS(desc, bit);
    // set clock
    SetDTR(desc, 0);
    // wait half period
    usleep(period >> 1);
    return 0;
}

// Write string of bits via unsigned character pointer.  Least significant byte must be in first
// byte position (byte order in memory is reversed from transmission order).  Least significant
// bit goes first.
int WriteSerialBitString(int desc, unsigned char *bits, int length, int period) {
    unsigned char byte;
    int i, bit;

    // handle full characters
    while (length / CHAR_BIT) {
        byte = *bits++;
        for (i = 0; i < CHAR_BIT; i++) {
            bit = byte & 1;
            byte >>= 1;
            WriteSerialBit(desc, bit, period);
        }
        length -= CHAR_BIT;
    }
    // handle left over bits, if any
    byte = *bits++;
    while (length) {
        bit = byte & 1;
        byte >>= 1;
        WriteSerialBit(desc, bit, period);
        length -= 1;
    }
    // negate clock
    SetDTR(desc, 1);
    return 0;
}

// Program the output frequency of an AD9851 DDS fed by a 30.0 MHz oscillator.

int SerialSetFrequency(int fd, double freq) {
    unsigned char zeros[] = {0, 0, 0, 0, 0};
    unsigned char tuning[5 ];

    // Strobe the DDS once to place in serial mode.  If already in serial mode, this just
    // reloads the working register from the shift register.
    PulseTXD(fd, SERIAL_CLOCK_PERIOD);

    // Send 40 bits of zero to clear register and strobe
    WriteSerialBitString(fd, zeros, 40, SERIAL_CLOCK_PERIOD);
    PulseTXD(fd, SERIAL_CLOCK_PERIOD);
    usleep(SERIAL_CLOCK_PERIOD * 30);

    // Generate message - tuning word = freq / clock * 2^32 where F is the desired frequency in
    // Hz, clock is 180 MHz (for DDS 60 board)
    unsigned int tuning_word = freq / 180.0L * 4294967296.0;

    printf("For %lf MHz tuning word is 0x%0x\n", freq, tuning_word);

    // reverse order of tuning word bytes
    tuning[0] = tuning_word >> 0;
    tuning[1] = tuning_word >> 8;
    tuning[2] = tuning_word >> 16;
    tuning[3] = tuning_word >> 24;
    tuning[4] = 0x01;  // refclk * 6, no phase

    // Send 40 bits of data and strobe
    WriteSerialBitString(fd, tuning, 40, SERIAL_CLOCK_PERIOD);
    PulseTXD(fd, SERIAL_CLOCK_PERIOD);
    usleep(SERIAL_CLOCK_PERIOD * 30);
    return 0;
}

#ifdef TEST
int main(int argc, char *argv[]) {
    int fd, status, index, count;
#if TEST_SIMPLE
    int i, j;
#endif
    char *port;
    struct termios tio;
    //unsigned char message[] = {0xA9, 0x22, 0x21, 0x08, 0x89, 0x25};

    port = argv[1];
    fd = AcquireSerialPort(port);
    if (fd == 1) {
        fprintf(stderr, "%s open() failed: %s\n", port, strerror(errno));
        return 1;
    }

    tcgetattr(fd, &tio);          /* get the termio information */
    tio.c_cflag &= ~HUPCL;        /* clear the HUPCL bit */
    tcsetattr(fd, TCSANOW, &tio); /* set the termio information */

#ifdef TEST_SIMPLE
    printf("test simple\n");
    if (argv[2]) {
        printf("set DTR\n");
        SetDTR(fd, argv[2][0] == '1');
    }

    if (argv[3]) {
        printf("set RTS\n");
        SetRTS(fd, argv[3][0] == '1');
    }
    
    count = 0;
    if (argv[4]) {
        count = atoi(argv[4]);
    }

    // pulse the TXD light
    for (j = 100000; j > 1; j /= 2) {
        printf("pulse TXD\n");
        for (i = 0; i < 4; i++) {
            PulseTXD(fd, j);
        }
    }

    if ((status = GetCTS(fd)) == -1)
        printf("TIOCMGET failed: %s\n", strerror(errno));
    else {
        if (status == 0)
            fputs("CTS is not set, ", stdout);
        else
            fputs("CTS is set, ", stdout);
        if ((status = GetDSR(fd)) == -1)
            printf("TIOCMGET failed: %s\n", strerror(errno));
        else {
            if (status == 0)
                fputs("DSR is not set", stdout);
            else
                fputs("DSR is set", stdout);
        }
        puts(".");
    }
#endif

#ifdef TEST_MEDIUM
    usleep(3000000);

    // Write a bit string at 1/2 Hz
    WriteSerialBitString(fd, message, 47, 2000000);
#endif

    for (index = 0; index < count; index++) {
        SerialSetFrequency(fd, 14.84L);
        usleep(1000000);
    }

    ReleaseSerialPort(fd);
    return 0;
}
#endif

